package owg.util.editor2d.operations;

import java.lang.reflect.Field;
import owg.util.data.Hidden;
import owg.util.data.PField;
import owg.util.data.PFieldElement;
import owg.util.data.PFieldList;
import owg.util.data.KryoInstance.K;
import owg.util.data.ReflectionUtil;
import owg.util.editor2d.Editor2D;
import owg.util.editor2d.entity.Input;
import owg.util.editor2d.entity.Duplex;
import owg.util.editor2d.entity.Output;
import owg.util.editor2d.entity.Placeable2D;
import owg.util.editor2d.modes.Link;

public class LinkOperation extends Operation
{

	/*Information that is used to unlink.
	 * If the objects are un-created, recreated and re-destroyed, then this list will be updated, ensuring safety of operation.*/
	protected PFieldElement srcConnectorRestoreValue;
	protected PFieldElement dstConnectorRestoreValue;
	protected PField<?> srcConnector;
	protected PField<?> dstConnector;
	protected Placeable2D srcObject;
	protected Placeable2D dstObject;
	
	private int srcObjectIndex;
	private int dstObjectIndex;
	private String srcFieldName;
	private String dstFieldName;
	@K private LinkOperation(){}
	/**
	 * @param srcObjectIndex The index, in the world objects list, of the world object which will be affected.
	 */
	public LinkOperation(Editor2D e, int srcObjectIndex, String srcFieldName, int dstObjectIndex, String dstFieldName)
	{
		this.srcObjectIndex = srcObjectIndex;
		this.srcFieldName = srcFieldName;
		this.dstObjectIndex = dstObjectIndex;
		this.dstFieldName = dstFieldName;
	}	
	@Override
	public void apply(Editor2D w) throws OperationFailedException
	{
		try 
		{
			applyWithoutCatch(w);
		}
		catch(IllegalArgumentException e)
		{
			throw new OperationFailedException("Unexpected error during linking: ", e);
		}
	}
	/**Execute the linking without rethrowing {@link IllegalArgumentException}s as {@link OperationFailedException}s.
	 * This is useful for verifying that the LinkOperation works, the first time it is executed.
	 * Implementations are allowed to throw a {@link IllegalArgumentException} the first time the operation is executed,
	 * to indicate a non-standard constraint such as avoiding cycles in a link graph.
	 * However, if an {@link IllegalArgumentException} is thrown upon a redo or undo, then this is symptomatic
	 * of a programming error in the application; The constraints should only depend on the link graph itself, 
	 * and the link graph will be the same when the operation is redone/undone. 
	 * Note that this has absolutely no bearing on exceptions arising from other types of programming errors.*/
	public void applyWithoutCatch(Editor2D w) throws IllegalArgumentException
	{
		if(srcObjectIndex >= w.worldObjectsImmutable.size())
			throw new IllegalArgumentException("The src index "+srcObjectIndex+" is out of range(n="+w.worldObjectsImmutable.size()+")");
		Placeable2D srcObject = w.worldObjectsImmutable.get(srcObjectIndex).a;
		if(dstObjectIndex >= w.worldObjectsImmutable.size())
			throw new IllegalArgumentException("The dst index "+dstObjectIndex+" is out of range(n="+w.worldObjectsImmutable.size()+")");
		Placeable2D dstObject = w.worldObjectsImmutable.get(dstObjectIndex).a;
		
		Field srcField = ReflectionUtil.getField(srcObject, srcFieldName);
		if(srcField == null)
			throw new IllegalArgumentException(srcObject.getName()+" has no field with the name "+srcFieldName); 
		
		Field dstField = ReflectionUtil.getField(dstObject, dstFieldName);
		if(dstField == null)
			throw new IllegalArgumentException(dstObject.getName()+" has no field with the name "+dstFieldName);
		
		Object srcFieldValue;
		Object dstFieldValue;
		byte srcFunc = 0;
		byte dstFunc = 0;
		try 
		{
			if(srcField.getAnnotation(Hidden.class) != null)
				System.err.println("Warning: The linked srcField "+srcField+" is Hidden...");
			if(dstField.getAnnotation(Hidden.class) != null)
				System.err.println("Warning: The linked dstField "+dstField+" is Hidden...");
			
			if(srcField.getAnnotation(Duplex.class) != null)
				srcFunc |= Link.MASK_INPUT_AND_OUTPUT;
			if(srcField.getAnnotation(Output.class) != null)
				srcFunc |= Link.MASK_OUTPUT;
			if(srcField.getAnnotation(Input.class) != null)
				srcFunc |= Link.MASK_INPUT;
			
			if(dstField.getAnnotation(Duplex.class) != null)
				dstFunc |= Link.MASK_INPUT_AND_OUTPUT;
			if(dstField.getAnnotation(Output.class) != null)
				dstFunc |= Link.MASK_OUTPUT;
			if(dstField.getAnnotation(Input.class) != null)
				dstFunc |= Link.MASK_INPUT;
			
			srcField.setAccessible(true);
			dstField.setAccessible(true);
			srcFieldValue = srcField.get(srcObject);
			dstFieldValue = dstField.get(dstObject);
		}
		catch(Exception e)
		{
			throw new IllegalArgumentException("Failed to get field value: ",e);
		}
		
		if(!(srcFieldValue instanceof PField))
			throw new IllegalArgumentException("The field with name "+srcFieldName+" on "+srcObject+" contained "+
				srcFieldValue+" which is not an instance of "+PField.class.getSimpleName()+"!");
		if(!(dstFieldValue instanceof PField))
			throw new IllegalArgumentException("The field with name "+dstFieldName+" on "+dstObject+" contained "+
				dstFieldValue+" which is not an instance of "+PField.class.getSimpleName()+"!");
		
		PField<?> srcConnector = (PField<?>)srcFieldValue;
		PField<?> dstConnector = (PField<?>)dstFieldValue;
		
		if(srcFunc == 0)
			throw new IllegalArgumentException(srcFieldName+" on "+srcObject+" is sexless!");
		if(dstFunc == 0)
			throw new IllegalArgumentException(dstFieldName+" on "+dstObject+" is sexless!");
		
		String err = Link.fits(srcObject.getClass(), srcConnector.getType(), srcFunc, dstObject.getClass(), dstConnector.getType(), dstFunc); 
		if(err != null)
			throw new IllegalArgumentException(srcFieldName+" on "+srcObject+" cannot connect to "+dstFieldName+" on "+dstObject+" because: "+err);
		
		if(!(srcObject instanceof PFieldElement))
			throw new IllegalArgumentException(srcObject+" was not an instance of "+PFieldElement.class.getSimpleName()+"!");
		if(!(dstObject instanceof PFieldElement))
			throw new IllegalArgumentException(dstObject+" was not an instance of "+PFieldElement.class.getSimpleName()+"!");
		

		//Use a two-way handshake:
		//Connect/disconnect one way, remembering original value
		//Connect/disconnect other way
		//If error occurs after first connect/disconnect, 
		//then restore the remembered value, hopefully doing as little damage as possible.
		PFieldElement srcConnectorRestoreValue = null;
		PFieldElement dstConnectorRestoreValue = null;
		boolean srcConnectorCompleted = false;
		try {
			if(srcConnector instanceof PFieldList<?>)
				add(srcConnector, dstObject);
			else
			{
				srcConnectorRestoreValue = srcConnector.get();
				set(srcConnector, dstObject);
			}
			srcConnectorCompleted = true;

			if(dstConnector instanceof PFieldList<?>)
				add(dstConnector, srcObject);
			else
			{
				dstConnectorRestoreValue = dstConnector.get();
				set(dstConnector, srcObject);
			}
			//The deed is done, remember the values so unlinker can access them
			this.srcObject = srcObject;
			this.dstObject = dstObject;
			this.srcConnector = srcConnector;
			this.dstConnector = dstConnector;
			this.srcConnectorRestoreValue = srcConnectorRestoreValue;
			this.dstConnectorRestoreValue = dstConnectorRestoreValue;
		}
		catch(Exception e)
		{
			if(srcConnectorCompleted)
			{
				//Note: if an error occurs here, we are boned.
				//Can't really do anything about that except fix the application.
				if(srcConnectorRestoreValue == null)
					srcConnector.remove(dstObject);
				else
					set(srcConnector, srcConnectorRestoreValue);
					
			}
			throw new IllegalArgumentException(e);
		}
	}
	protected<X extends PFieldElement> void add(PField<X> connector, Object object)
	{
		connector.add(connector.cast(object));
	}
	protected<X extends PFieldElement> void set(PField<X> connector, Object object)
	{
		connector.set(connector.cast(object));
	}
}